Skip to content

feat(i18n): add internationalization support for Chinese, Japanese, and Korean#87

Open
miclle wants to merge 5 commits intogoplus:mainfrom
miclle:main
Open

feat(i18n): add internationalization support for Chinese, Japanese, and Korean#87
miclle wants to merge 5 commits intogoplus:mainfrom
miclle:main

Conversation

@miclle
Copy link
Copy Markdown

@miclle miclle commented Apr 18, 2026

Summary

  • Add i18n infrastructure: URL path prefix routing (/zh/, /ja/, /ko/), locale JSON files, translation template functions, per-language caching
  • Add language switcher dropdown (data-driven, supports adding more languages easily)
  • Add Chinese (zh), Japanese (ja), and Korean (ko) translations for all 112 tutorial sections
  • Add gen-translation.go tool for generating translation skeleton files
  • Keep original .gop source files untouched; translations overlay via .md files under locales/

How it works

  • Routing: /hello-world → English (default), /zh/hello-world → Chinese, /ja/hello-world → Japanese, /ko/hello-world → Korean
  • Translation files: locales/<lang>/<tutorial-dir>/<file>.md with --- separators matching .gop doc segments
  • UI text: locales/<lang>.json with UI strings and bilingual title translations (e.g., "Functions 関数")
  • Fallback: if a translation .md file is missing, the original English content is shown

Files changed

Area Files
Core i18n main.go
Templates templates/index.tmpl, templates/example.tmpl
Styles public/site.css (language switcher dropdown)
Locale data locales/en.json, locales/zh.json, locales/ja.json, locales/ko.json
Translations locales/zh/**/*.md (112 files), locales/ja/**/*.md (112 files), locales/ko/**/*.md (112 files)
Tool gen-translation.go (skeleton generator)

Test plan

  • go build . compiles successfully
  • / shows English index page (unchanged)
  • /zh/ shows Chinese index page with translated titles
  • /ja/ shows Japanese index page
  • /ko/ shows Korean index page
  • /zh/hello-world shows Chinese tutorial content
  • /ja/hello-world shows Japanese tutorial content
  • /ko/hello-world shows Korean tutorial content
  • Language switcher dropdown shows 4 languages and switches correctly
  • Arrow key navigation preserves current language
  • Next example links preserve current language

miclle added 4 commits April 18, 2026 20:59
- Add URL path prefix routing (/zh/) for language selection
- Add locale files (locales/en.json, locales/zh.json) for UI text and title translations
- Add Chinese translation .md files for all 112 tutorial sections
- Add language switcher dropdown in breadcrumb area
- Add per-language caching for parsed examples and rendered index pages
- Add gen-translation.go skeleton generator tool
- Keep original .gop source files untouched; translations overlay via .md files
Add localized aria-label text for the language switcher and keep the page templates aligned with the existing translation flow.
… switcher

- Register ja/ko in supportedLangs
- Add langEntry type and allLangs ordered list for template rendering
- Add currentLangName and allLangs template functions
- Refactor language switcher in index.tmpl and example.tmpl from hardcoded to data-driven
Copy link
Copy Markdown

@xgopilot xgopilot Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

The i18n infrastructure is well-structured overall — locale fallback, per-language caching, and the data-driven language switcher are solid. A few issues in the generation tool and locale files are worth addressing before merge.

Comment thread gen-translation.go Outdated
docText = strings.TrimPrefix(trimmed[2:], " ")
} else if strings.HasPrefix(trimmed, "#") && !strings.HasPrefix(trimmed, "#!") {
isDoc = true
docText = "##" + trimmed
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: double # prefix produces malformed headings

trimmed already starts with #, so "##" + trimmed yields ## # Title — a level-2 heading whose display text is the literal string # Title. The same pattern exists in the production main.go parsing path, so the skeleton files and the rendered output will be consistent, but the heading levels will be wrong (H2 for content that was a bare # comment in the source).

If the intent is to promote .gop #-lines to Markdown H2s, strip the leading # first:

docText = "## " + strings.TrimPrefix(strings.TrimPrefix(trimmed, "#"), " ")

Or if the transformation in main.go should be the canonical one, keep both files in sync with the same logic.

Comment thread gen-translation.go Outdated
if fi.IsDir() {
name := fi.Name()
if len(name) > (chNumLen+1) && name[chNumLen] == '-' {
if _, e := strconv.Atoi(name[:1]); e == nil {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only the first character of the chapter prefix is validated as numeric

name[:1] is a single character. A directory named 1AB-Something would pass this check even though its prefix is not a valid three-digit number. Use name[:chNumLen] to validate the full prefix:

if _, e := strconv.Atoi(name[:chNumLen]); e == nil {

Comment thread gen-translation.go
mdPath := filepath.Join("locales", lang, dir, mdName)

// Skip if already exists
if _, err := os.Stat(mdPath); err == nil {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

os.Stat error is not distinguished from "file not found"

If os.Stat fails for a reason other than the file not existing (e.g. a permissions issue), the code silently proceeds to write a new file. Prefer:

if _, err := os.Stat(mdPath); err == nil {
    skipped++
    continue
} else if !os.IsNotExist(err) {
    fmt.Fprintf(os.Stderr, "Warning: cannot stat %s: %v\n", mdPath, err)
    continue
}

Comment thread gen-translation.go Outdated
}

// Write skeleton .md
skeleton := strings.Join(docs, "\n---\n")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--- without surrounding blank lines is parsed as a setext H2 heading

In CommonMark, a line of --- immediately following text is interpreted as a setext-style H2 heading for that text, not as a thematic break. This means the last line of each doc segment will be rendered as a heading rather than as a divider. Add blank lines around the separator:

skeleton := strings.Join(docs, "\n\n---\n\n")

Comment thread gen-translation.go Outdated
}

// Create lang directory
langDir := filepath.Join("locales", lang, dir)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

os.MkdirAll called once per source file rather than once per directory

All .gop/.xgo files in the same dir share the same langDir. Moving this call outside the inner fi loop (or hoisting it just before the inner loop begins) avoids the redundant syscall for every file after the first in each directory.

Comment thread gen-translation.go
if len(os.Args) < 2 {
fmt.Fprintln(os.Stderr, "Usage: go run gen-translation.go <lang> [tutorial-dir...]")
os.Exit(1)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Path traversal via unsanitized lang argument

lang is used directly in filepath.Join("locales", lang, ...) for both os.MkdirAll and os.WriteFile. While filepath.Join cleans the path, a value like ../../etc resolves outside locales/. Even for a //go:build ignore tool, validating the argument is a low-cost safeguard:

if !regexp.MustCompile(`^[a-z]{2,10}$`).MatchString(lang) {
    fmt.Fprintln(os.Stderr, "Error: lang must be a simple language code (e.g. zh, ja, ko)")
    os.Exit(1)
}

The same concern applies to directory values passed explicitly via os.Args[2:] — each entry should be checked to contain no path separators.

Comment thread locales/en.json
"no_content_after": ".",
"next_example": "Next example:",
"lang_switcher_label": "Switch language"
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

en.json is missing the titles object present in all other locale files

zh.json, ja.json, and ko.json all include a "titles" map of English tutorial names → localized names. en.json omits it entirely, making it structurally inconsistent with the other locale files. Since en.json is the canonical schema reference for contributors adding new locales, the missing key is likely to cause incomplete locale files in the future.

Consider adding identity mappings (English → English) or at minimum a comment/schema file in locales/ documenting the expected structure.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a gen-translation.go utility script to automate the creation of translation placeholders, alongside a comprehensive set of initial localized content for Japanese, Korean, and Chinese. The review feedback identifies a logic error in the script's directory prefix parsing and highlights several consistency issues in the translation files, specifically regarding the branding of 'XGo' and missing bilingual titles for the 'Hello World' tutorial.

Comment thread gen-translation.go Outdated
if fi.IsDir() {
name := fi.Name()
if len(name) > (chNumLen+1) && name[chNumLen] == '-' {
if _, e := strconv.Atoi(name[:1]); e == nil {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic strconv.Atoi(name[:1]) is fragile and potentially incorrect given that chNumLen is set to 3. If the directory naming convention uses a 3-digit prefix (e.g., 101-), it is safer to validate the entire prefix.

Suggested change
if _, e := strconv.Atoi(name[:1]); e == nil {
if _, e := strconv.Atoi(name[:chNumLen]); e == nil {

Comment thread locales/ja/117-Slices/slices-10.md Outdated
@@ -0,0 +1,10 @@
### スライスへの要素追加
スライスに新しい要素を追加するのは一般的な操作なので、Go は組み込みの append 関数を提供しています。組み込みパッケージのドキュメントに append の使い方が記載されています。
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The text refers to "Go" instead of "XGo". To maintain consistency with the rest of the documentation and the project's branding, this should be updated.

Suggested change
スライスに新しい要素を追加するのは一般的な操作なので、Go は組み込みの append 関数を提供しています。組み込みパッケージのドキュメントに append の使い方が記載されています。
### スライスへの要素追加
スライスに新しい要素を追加するのは一般的な操作なので、XGo は組み込みの append 関数を提供しています。組み込みパッケージのドキュメントに append の使い方が記載されています。

Comment thread locales/ko/117-Slices/slices-10.md Outdated
@@ -0,0 +1,10 @@
### 슬라이스에 요소 추가하기
슬라이스에 새 요소를 추가하는 것은 매우 일반적인 작업이므로, Go는 내장 append 함수를 제공합니다. 내장 패키지 문서에서 append의 사용법을 설명합니다.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The text refers to "Go" instead of "XGo". This should be updated for consistency.

Suggested change
슬라이스에 새 요소를 추가하는 것은 매우 일반적인 작업이므로, Go는 내장 append 함수를 제공합니다. 내장 패키지 문서에서 append의 사용법을 설명합니다.
### 슬라이스에 요소 추가하기
슬라이스에 새 요소를 추가하는 것은 매우 일반적인 작업이므로, XGo는 내장 append 함수를 제공합니다. 내장 패키지 문서에서 append의 사용법을 설명합니다.

Comment thread locales/zh/117-Slices/slices-10.md Outdated
@@ -0,0 +1,10 @@
### 向切片追加元素
向切片追加新元素是很常见的操作,因此 Go 提供了内置的 append 函数。内置包的文档中描述了 append 的用法。
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The text refers to "Go" instead of "XGo". This should be updated for consistency.

Suggested change
向切片追加新元素是很常见的操作,因此 Go 提供了内置的 append 函数。内置包的文档中描述了 append 的用法。
### 向切片追加元素
向切片追加新元素是很常见的操作,因此 XGo 提供了内置的 append 函数。内置包的文档中描述了 append 的用法。

Comment thread locales/ja.json
"titles": {
"Sequential programming": "Sequential Programming 順次プログラミング",
"Structured programming": "Structured Programming 構造化プログラミング",
"Hello world": "Hello World",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The title translation for "Hello world" is missing the Japanese part, unlike other entries which follow the bilingual format (e.g., "Values 値").

Suggested change
"Hello world": "Hello World",
"Hello world": "Hello World ハローワールド",

Comment thread locales/ko.json
"titles": {
"Sequential programming": "Sequential Programming 순차 프로그래밍",
"Structured programming": "Structured Programming 구조적 프로그래밍",
"Hello world": "Hello World",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The title translation for "Hello world" is missing the Korean part, unlike other entries which follow the bilingual format.

Suggested change
"Hello world": "Hello World",
"Hello world": "Hello World 헬로 월드",

Comment thread locales/zh.json
"titles": {
"Sequential programming": "Sequential Programming 顺序编程",
"Structured programming": "Structured Programming 结构化编程",
"Hello world": "Hello World",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The title translation for "Hello world" is missing the Chinese part, unlike other entries which follow the bilingual format.

Suggested change
"Hello world": "Hello World",
"Hello world": "Hello World 你好世界",

- gen-translation.go: validate lang argument against path traversal
- gen-translation.go: validate full 3-digit prefix instead of first char
- gen-translation.go: distinguish os.Stat errors from file-not-found
- gen-translation.go: hoist os.MkdirAll outside inner file loop
- gen-translation.go: fix --- separator with blank lines for CommonMark
- gen-translation.go: fix heading prefix to avoid ## # malformed headings
- Fix "Go" → "XGo" in slices-10.md for zh/ja/ko
- Add empty titles object to en.json for structural consistency
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant